/*
 * Galaxium Messenger
 * 
 * Copyright (C) 2007 Ben Motmans <ben.motmans@gmail.com>
 * Copyright (C) 2008 Philippe Durand <draekz@gmail.com>
 * 
 * License: GNU General Public License (GPL)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Collections.Generic;

using Anculus.Core;
using Galaxium.Core;
using Org.Mentalis.Network.ProxySocket;

namespace Galaxium.Protocol
{
	public class TCPConnection : AbstractConnection
	{
		protected Socket _socket;
		protected bool _sending;
	
		public Socket Socket
		{
			get { return _socket; }
		}
		
		public override bool IsConnected
		{
			get
			{
				lock (_lock)
				{
					if (_socket != null)
					{
						// Socket.Connected doesn't tell us if the socket is actually connected...
						// http://msdn2.microsoft.com/en-us/library/system.net.sockets.socket.connected.aspx
						
						bool blocking = _socket.Blocking;
						try
						{
							_socket.Blocking = false;
							_socket.Send (new byte[0], 0, 0);
							return true;
						}
						catch (SocketException ex)
						{
							// 10035 == WSAEWOULDBLOCK
							if (ex.NativeErrorCode.Equals(10035))
								return true;
						} finally {
							_socket.Blocking = blocking;
						}
					}

					return false;
				}
			}
		}
		
		// Return true if we were connected at the last operation
		public bool WasConnected
		{
			get
			{
				lock (_lock) {
					if (_socket != null)
						return _socket.Connected;
					return false;
				}
			}
		}
		
		public override bool IsSending
		{
			get { return _sending; }
		}
		
		public TCPConnection (ISession session, IConnectionInfo connectionInfo) : base (session, connectionInfo)
		{
			BufferInitialize ();
		}
		
		public override void Connect ()
		{
			Log.Debug ("Connect to {0}:{1}", _connectionInfo.HostName, _connectionInfo.Port);
			
			_intendedDisconnect = false;
			
			try
			{
				lock (_lock)
				{
					ProxySocket socket = new ProxySocket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
					
					IConfigurationSection section = Configuration.Proxy.Section;
					
					if (section.GetBool (Configuration.Proxy.UseSocksProxy.Name, Configuration.Proxy.UseSocksProxy.Default))
					{
						ProxyType type = (ProxyType)section.GetInt (Configuration.Proxy.SocksType.Name, Configuration.Proxy.SocksType.Default);
						
						switch (type)
						{
							case ProxyType.Socks4:
								socket.ProxyType = ProxyTypes.Socks4;
								break;
							default:
							case ProxyType.Socks5:
								socket.ProxyType = ProxyTypes.Socks5;
								break;
						}
						
						socket.ProxyUser = section.GetString (Configuration.Proxy.SocksUsername.Name, Configuration.Proxy.SocksUsername.Default);
						socket.ProxyPass = section.GetString (Configuration.Proxy.SocksPassword.Name, Configuration.Proxy.SocksPassword.Default);
						
						socket.ProxyEndPoint = GetAddress (section.GetString (Configuration.Proxy.SocksHost.Name), section.GetInt (Configuration.Proxy.SocksPort.Name));
					}
					else
					{
						socket.ProxyType = ProxyTypes.None;
					}
					
					_socket = socket;
					_socket.Blocking = true;
					_socket.BeginConnect (GetAddress (_connectionInfo.HostName, _connectionInfo.Port), new AsyncCallback (EndConnectCallback), _socket);
				}
			}
			catch (SocketException ex)
			{
				Log.Error (ex, "Unable to connect to server ["+ConnectionInfo.HostName+"].");
				
				ThreadUtility.SyncDispatch (new VoidDelegate (delegate
				{
					OnErrorOccurred (new ConnectionErrorEventArgs (this, "PROTOCOL_CONNECT", "Unable to connect to server."));
				}));
			}
		}
		
		protected override void Dispose (bool disposing)
		{
			if (!_disposed)
			{
				if (disposing)
				{
					if (WasConnected)
					{
						Log.Debug("Disconnecting during disposing of abstract connection.");
						Disconnect ();
					}
					
					_socket = null;
					
					BufferUnload ();
				}
			}
			
			_disposed = true;
		}
		
		public override void Reconnect ()
		{
			Disconnect ();
			Connect ();
		}

		public override void Disconnect ()
		{
			_intendedDisconnect = true;
			_sending = false;
			
			if (WasConnected)
			{
				lock (_lock)
				{
					if (_socket != null && _socket.Connected)
					{
						try
						{
							_socket.Shutdown (SocketShutdown.Both);
							_socket.Close ();
							_socket = null;
						}
						catch (SocketException e)
						{
							//Log.Warn ("Unable to cleanly shutdown TCP socket: "+e.Message);
						}
					}
				}
				
				ThreadUtility.SyncDispatch (new VoidDelegate (delegate
				{
					OnClosed (new ConnectionEventArgs (this));
				}));
			}
		}
		
		protected virtual void EndConnectCallback (IAsyncResult asyncResult)
		{
			try
			{
				if (asyncResult.IsCompleted)
				{
					lock (_lock)
					{
						Socket socket = asyncResult.AsyncState as Socket;

						if (IsConnected) {
							socket.EndConnect (asyncResult);
							socket.Blocking = false;
						} else
						{
							ThreadUtility.SyncDispatch (new VoidDelegate (delegate
							{
								OnErrorOccurred (new ConnectionErrorEventArgs (this, "SocketError", "Unable to connect socket."));
							}));
							
							return;
						}
					}
					
					ThreadUtility.SyncDispatch (new VoidDelegate (delegate
					{
						OnEstablished (new ConnectionEventArgs (this));
						
						OnAfterConnect (new ConnectionEventArgs (this));
					}));
					                            
					BeginDataReceive ();
				}
				else
					throw new ApplicationException ();
			}
			catch (Exception ex)
			{
				Log.Error (ex, "Unable to connect to server ["+ConnectionInfo.HostName+"].");
				
				ThreadUtility.SyncDispatch (new VoidDelegate (delegate
				{
					OnErrorOccurred (new ConnectionErrorEventArgs (this, "PROTOCOL_CONNECT", "Unable to connect to server."));
				}));
				
				Disconnect ();
			}
		}

		protected virtual void BeginDataReceive ()
		{
			byte[] buffer = GetBuffer ();
			
			lock (_lock)
			{
				if (_socket != null && _socket.Connected)
					_socket.BeginReceive (buffer, 0, buffer.Length, SocketFlags.None, new AsyncCallback (EndReceiveCallback), new SocketStore (_socket, buffer));
			}
		}

		protected virtual byte[] GetBuffer ()
		{
			return BufferUtility.Allocate (BufferSize.Protocol);
		}

		protected virtual void FreeBuffer (ref byte[] buffer)
		{
			BufferUtility.Free (BufferSize.Protocol, ref buffer);
		}

		protected virtual void BufferInitialize ()
		{
			BufferUtility.IncreaseCapacity (BufferSize.Protocol);
		}

		protected virtual void BufferUnload ()
		{
			BufferUtility.DecreaseCapacity (BufferSize.Protocol);
		}
		
		public override void Send (byte[] data)
		{
			ThrowUtility.ThrowIfNull ("data", data);
			
			try
			{
				lock (_lock)
				{
					if (_socket != null && _socket.Connected)
					{
						_sending = true;
						_socket.BeginSend (data, 0, data.Length, SocketFlags.None, new AsyncCallback (EndSendCallback), _socket);
					}
				}
			}
			catch (SocketException ex)
			{
				if (!_intendedDisconnect)
				{
					Log.Error (ex, "Send");
					
					ThreadUtility.SyncDispatch (new VoidDelegate (delegate
					{
						OnErrorOccurred (new ConnectionErrorEventArgs (this, "PROTOCOL_SEND", "Data send error."));
					}));
				}
			}
		}
		
		protected virtual void EndSendCallback (IAsyncResult asyncResult)
		{
			if (asyncResult.IsCompleted)
			{
				lock (_lock)
				{
					Socket socket = asyncResult.AsyncState as Socket;
					
					if (socket != null && socket.Connected)
					{
						socket.EndSend (asyncResult);
						_sending = false;
					}
				}
						
				ThreadUtility.SyncDispatch (new VoidDelegate (delegate
				{
					OnSendComplete (EventArgs.Empty);
				}));
			}
		}

		protected virtual void EndReceiveCallback (IAsyncResult asyncResult)
		{
			try
			{
				int len = 0;
				byte[] buffer = null;
				
				if (asyncResult.IsCompleted)
				{
					lock (_lock)
					{
						SocketStore store = asyncResult.AsyncState as SocketStore;
						Socket socket = store.Socket;
						
						if (socket != null && socket.Connected)
						{
							len = socket.EndReceive (asyncResult);
							buffer = store.Buffer;
						}
					}
				}
				else
				{
					SocketStore store = asyncResult.AsyncState as SocketStore;
					buffer = store.Buffer;
					FreeBuffer (ref buffer);
					return;
				}

				if (len == 0)
				{
					FreeBuffer (ref buffer);
					
					ThreadUtility.SyncDispatch (new VoidDelegate (delegate
					{
						OnErrorOccurred (new ConnectionErrorEventArgs (this, "PROTOCOL_RECEIVE_EMPTY", "Connection closed by server."));
					}));
					
					Log.Error("Connection closed by server ["+ConnectionInfo.HostName+"].");
					Disconnect ();
					
					return;
				}
				
				ThreadUtility.SyncDispatch (new VoidDelegate (delegate
				{
					OnDataReceived (new ConnectionDataEventArgs (this, buffer, len));
				}));
				
				FreeBuffer (ref buffer);

				BeginDataReceive ();
			}
			catch (Exception ex)
			{
				Log.Debug ("The connection ended "+(_intendedDisconnect ? "as expected." : "unexpectedly."));
				
				if (!_intendedDisconnect)
				{
					Log.Error (ex, "EndReceiveCallback");
					
					ThreadUtility.SyncDispatch (new VoidDelegate (delegate
					{
						OnErrorOccurred (new ConnectionErrorEventArgs (this, "PROTOCOL_RECEIVE", "Connection closed."));
					}));
					
					Disconnect ();
				}
			}
		}
		
		private class SocketStore
		{
			public Socket Socket;
			public byte[] Buffer;

			public SocketStore (Socket socket, byte[] buffer)
			{
				Socket = socket;
				Buffer = buffer;
			}
		}
	}
}
